﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Windows.Navigation;
using System.Windows.Threading;
using Com.PhilChuang.Pmvp.Ui.Views.Impl.Pages;
using Com.PhilChuang.Pmvp.Ui.Views.Impl.ViewModels;
using Com.PhilChuang.Utils;
using Com.PhilChuang.Utils.Mobile;

namespace Com.PhilChuang.Pmvp.Ui.Views.Impl.ViewFacades
{
	public abstract class AbstractViewFacade<TPage, TPageModel> : IView
		where TPage : PageBase<TPageModel>
		where TPageModel : AbstractViewModel
	{
		#region Constants
		private const int SHOWN_WAIT_TIMEOUT_MS = 10 * 1000; // 10s
		#endregion

		#region Events
		/// <summary>
		/// Fires when the view is loaded, non-async
		/// </summary>
		public event Action Loaded = delegate { };
		/// <summary>
		/// Fires every time the view is shown, non-async
		/// </summary>
		public event Action Shown = delegate { };
		/// <summary>
		/// Fires when the user initiates the close, non-async
		/// </summary>
		public event EventHandler<CancelEventArgs> DoClose = delegate { };
		/// <summary>
		/// Fires when the view is closed, non-async
		/// </summary>
		public event Action Closed = delegate { };
		/// <summary>
		/// Fires when the view is being disposed
		/// </summary>
		public event Action Unloaded = delegate { }; 
		#endregion

		#region Instance Variables
		private NavigationHelper m_NavHelper;
		private readonly ManualResetEvent m_NavHelperMre = new ManualResetEvent (false);
		protected TPageModel myViewModel;
		protected TPage myPage;
		private readonly Object lock_myPage = new object ();
		private readonly ManualResetEvent m_LoadedMre = new ManualResetEvent (false); // used to ensure that Loaded and Shown don't happen simultaneously
		private readonly ManualResetEvent m_ShownMre = new ManualResetEvent (false); // used to ensure that Shown has finished running
		private bool m_IsClosing = false;
		private readonly Object lock_m_CloseExecuted = new object (); // used to ensure that Close only gets called once
		private bool m_CloseExecuted = false; // used to ensure that Close only gets called once
		#endregion
		
		#region Properties
		public bool IsDisposed { get; private set; }
		protected bool IsDisposing { get; private set; }
		#endregion

		#region Constructors / Dispose
		protected AbstractViewFacade ()
		{
			DebugWriteLine ("new");
		}

		private readonly Object lock_Dispose = new object ();
		/// <summary>
		/// Disposes of resources.   Should be called from code only by Presenter
		/// </summary>
		public virtual void Dispose ()
		{
			DebugWriteLine ("Dispose");
			if (IsDisposing || IsDisposed) return;
			lock (lock_Dispose)
			{
				if (IsDisposing || IsDisposed) return;

				IsDisposing = true;
				if (myPage != null)
				{
					if (!myPage.Dispatcher.InvokeIfRequired (UnWirePage))
						UnWirePage ();
				}
				myViewModel = null;
				myPage = null;
				m_NavHelper = null;
				
				IsDisposing = false;
				IsDisposed = true;
			}
		}
		#endregion

		private void NavHelperNavigated (object sender, NavigationEventArgs e)
		{
			m_NavHelper.Navigated -= NavHelperNavigated;
			if (myPage != null) return;
			lock (lock_myPage)
			{
				if (myPage != null) return;

				if (!(e.Content is TPage))
				{
					myPage = null;
					return;
				}
				myPage = (TPage) m_NavHelper.CurrentPage;
				WireUpPage ();
				FireLoaded ();
				m_NavHelperMre.Set ();
			}
		}

		/// <summary>
		/// Navigates to and displays the Page if it is not the current Page
		/// </summary>
		protected void ShowPage ()
		{
			ShowPage (null);
		}

		/// <summary>
		/// Navigates to and displays the Page if it is not the current Page
		/// </summary>
		protected void ShowPage (IList<KeyValuePair<String, String>> parms)
		{
			if (myPage != null) return;
			if (IsDisposedOrDisposing ()) return;

			lock (lock_myPage)
			{
				if (myPage != null) return;
				if (IsDisposedOrDisposing ()) return;

				m_NavHelper = AppServiceLocator.Instance.GetImpl<NavigationHelper> ();
				m_NavHelper.Navigated += NavHelperNavigated;
				m_NavHelperMre.Reset ();
				m_NavHelper.NavigateTo<TPage> (parms);
			}
			m_NavHelperMre.WaitOne (); // wait for the page to be navigated to
		}

		/// <summary>
		/// Unsubscribes Page event handlers.
		/// Call base impl as last step.
		/// </summary>
		protected virtual void UnWirePage ()
		{
			if (myPage == null) return;

			// IView
			myPage.Unloaded -= FireUnloaded;
			myPage.Hidden -= FireClosed;
			myPage.BackKeyPress -= FireDoClose;
			myPage.Shown -= FireShown;
			myPage.PageLoaded -= FireLoadedAndCheckFireReload;

			myPage.ViewModel = null;
		}

		/// <summary>
		/// Subscribes Page event handlers.
		/// Call base impl as first step.
		/// </summary>
		protected virtual void WireUpPage ()
		{
			if (myPage == null) return;

			myPage.ViewModel = myViewModel;

			// IView
			myPage.PageLoaded += FireLoadedAndCheckFireReload;
			myPage.Shown += FireShown;
			myPage.BackKeyPress += FireDoClose;
			myPage.Hidden += FireClosed;
			myPage.Unloaded += FireUnloaded;
		}

		#region Events
		protected virtual void FireLoaded ()
		{
			DebugWriteLine ("FireLoaded");
			Loaded ();
			m_LoadedMre.Set ();
		}

		protected virtual void FireLoadedAndCheckFireReload ()
		{
			DebugWriteLine ("FireLoadedAndCheckFireReload");
			Loaded ();
			if (myViewModel == null || !myViewModel.HasBeenLoaded)
				FireReload ();
			m_LoadedMre.Set ();
		}

		/// <summary>
		/// Method to check for any URL parameters and fire a reload event
		/// </summary>
		protected virtual void FireReload ()
		{
			DebugWriteLine ("FireReload");
			// does nothing, override in subclass
		}

		protected virtual void FireShown ()
		{
			DebugWriteLine ("FireShown");
			m_ShownMre.Reset ();
			Shown ();
			m_ShownMre.Set ();
		}

		protected virtual void FireDoClose (Object sender, CancelEventArgs e)
		{
			DebugWriteLine ("FireDoClose");
			if (m_IsClosing)
			{
				e.Cancel = true;
				return;
			}
			DoClose (sender, e);
		}

		protected virtual void FireClosed ()
		{
			DebugWriteLine ("FireClosed");
			Closed ();
		}

		/// <summary>
		/// Fires the Closed event when the Page has been unloaded.
		/// Call base impl as the last step.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		protected virtual void FireUnloaded (Object sender, EventArgs e)
		{
			DebugWriteLine ("FireUnloaded");
			if (myViewModel != null) // find out why this is null sometimes
				myViewModel.HasBeenUnloaded = true;
			Unloaded ();
		}
		#endregion

		#region Implementation of IView
		#region Functionality
		public virtual void EnableAllControls (bool enabled)
		{
			if (DoNotAccessViewModel ()) return;

			Invoke (() => {
			        	if (IsDisposedOrDisposing ()) return;
			        	myViewModel.AllControlsEnabled = enabled;
			        });
		}

		public virtual void Close ()
		{
			DebugWriteLine ("Close");
			
			if (m_IsClosing) return;
			m_IsClosing = true;
			if (IsDisposedOrDisposing ()) return;
			if (myPage == null) return;

			EnableAllControls (false); // prevent any other actions from occuring
			WaitForShownThenBeginInvokeAsync (
				() => {
					if (m_CloseExecuted) return;
					lock (lock_m_CloseExecuted)
					{
						if (m_CloseExecuted) return;
						myPage.NavigationService.GoBack ();
						m_CloseExecuted = true;
					}
				});
		}

		public virtual void Show ()
		{
			DebugWriteLine ("Show");
			if (myPage != null) return;

			ShowPage ();
		}
		#endregion

		#region Utility methods
		public virtual void DisplayErrorMessage (string text, string caption)
		{
			if (IsDisposedOrDisposing ()) return;

			ShowPage ();
			myPage.DisplayErrorMessage (text, caption);
		}

		public virtual void DisplayMessage (string text, string caption)
		{
			if (IsDisposedOrDisposing ()) return;

			ShowPage ();
			myPage.DisplayMessage (text, caption);
		}

		/// <summary>
		/// Displays a yes/no dialog to the user
		/// </summary>
		/// <param name="text"></param>
		/// <param name="caption"></param>
		/// <returns>user selected yes</returns>
		public virtual bool DisplayYesNoChoice (string text, string caption)
		{
			if (IsDisposedOrDisposing ()) return false;

			ShowPage ();
			return myPage.DisplayYesNoChoice (text, caption);
		}
		#endregion
		#endregion

		#region Utility methods
		protected Dispatcher GetDispatcher ()
		{
			return myPage != null 
				? myPage.Dispatcher 
				: m_NavHelper != null 
					? m_NavHelper.CurrentPage.Dispatcher 
					: null;
		}

		protected void ThrowIfUiThread ()
		{
			var d = GetDispatcher ();
			if (d != null && d.CheckAccess ())
				throw new Exception ("This method cannot be called on the UI thread");
		}

		protected void InvokeSet<T> (Action<T> a, T value)
		{
			var dispatcher = GetDispatcher ();
			if (dispatcher != null && dispatcher.InvokeIfRequired (() => a (value)))
				return;

			a (value);
		}

		protected T InvokeGet<T> (Func<T> f)
		{
			T result = default (T);
			var dispatcher = GetDispatcher ();
			if (dispatcher != null && dispatcher.InvokeIfRequired (() => result = f ()))
				return result;

			return f ();
		}

		/// <summary>
		/// Asynchronously invokes on the Dispatcher, fails silently if the dispatcher is unavailable
		/// </summary>
		/// <param name="a"></param>
		protected void BeginInvoke (Action a)
		{
			var d = GetDispatcher ();
			if (d == null || IsDisposedOrDisposing ()) return;
			d.BeginInvoke (a);
		}

		/// <summary>
		/// Calls WaitForShownToFinish then BeginInvoke and adds a check for IsDisposedOrDisposing
		/// </summary>
		/// <param name="a"></param>
		protected void WaitForShownThenBeginInvokeAsync (Action a)
		{ WaitForShownThenBeginInvokeAsync (a, null); }

		/// <summary>
		/// Calls WaitForShownToFinish then BeginInvoke and adds a check for IsDisposedOrDisposing
		/// </summary>
		/// <param name="a"></param>
		/// <param name="delayMs"></param>
		protected void WaitForShownThenBeginInvokeAsync (Action a, int? delayMs)
		{
			ThreadPool.QueueUserWorkItem (
				obj =>
				{
					if (!m_ShownMre.WaitOne (SHOWN_WAIT_TIMEOUT_MS)) return;
					if (IsDisposedOrDisposing ()) return;
					if (delayMs != null) Thread.Sleep (delayMs.Value);
					BeginInvoke (() =>
					{
						if (IsDisposedOrDisposing ()) return;
						a ();
					});
				});
		}

		/// <summary>
		/// Synchronously invokes on the Dispatcher (if required), fails silently if the dispatcher is unavailable
		/// </summary>
		/// <param name="a"></param>
		protected void Invoke (Action a)
		{
			if (IsDisposedOrDisposing ()) return;
			var d = GetDispatcher ();
			if (d == null) return;
			if (!d.InvokeIfRequired (a))
				a ();
		}

		protected bool IsDisposedOrDisposing ()
		{ return IsDisposing || IsDisposed; }

		protected bool DoNotAccessViewModel ()
		{ return IsDisposedOrDisposing () || myViewModel == null || myViewModel.HasBeenUnloaded; }

		protected void DebugWriteLine (String msg)
		{
#if DEBUG
			System.Diagnostics.Debug.WriteLine (String.Format ("[{0}]\t{1}\t{2}", System.Threading.Thread.CurrentThread.ManagedThreadId, GetType ().Name, msg));
#endif
		}
		#endregion
	}
}
